Day 19: 스트림 API
Stream API는 컬렉션 데이터를 선언적으로 처리하는 도구입니다. for 문 대신 “무엇을 할 것인지”만 기술하면 됩니다. 마치 공장 컨베이어 벨트처럼, 데이터가 파이프라인을 흐르면서 변환, 필터링, 집계됩니다.
Stream 기본 사용법
스트림 생성부터 중간 연산, 최종 연산까지의 기본 흐름입니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class StreamBasic {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 기본 파이프라인: 소스 -> 중간 연산 -> 최종 연산
int sumOfEven = numbers.stream() // 1. 스트림 생성
.filter(n -> n % 2 == 0) // 2. 중간: 짝수만
.mapToInt(Integer::intValue) // 3. 중간: int로 변환
.sum(); // 4. 최종: 합계
System.out.println("짝수 합: " + sumOfEven); // 30
// 스트림 생성 방법들
// 1. 컬렉션에서
List<String> names = List.of("홍길동", "김영희", "이철수");
Stream<String> nameStream = names.stream();
// 2. 배열에서
int[] arr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr);
// 3. Stream.of()
Stream<String> stream = Stream.of("A", "B", "C");
// 4. 범위 생성
IntStream range = IntStream.rangeClosed(1, 100); // 1~100
// 5. 무한 스트림
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
randoms.forEach(r -> System.out.printf("%.4f ", r));
System.out.println();
// 스트림은 한 번만 사용 가능! (재사용 불가)
Stream<Integer> oneTime = numbers.stream();
oneTime.forEach(System.out::print);
// oneTime.count(); // IllegalStateException!
}
}
중간 연산 (Intermediate Operations)
데이터를 변환하고 필터링하는 연산들입니다. 지연 평가(lazy evaluation)됩니다.
import java.util.List;
import java.util.stream.Stream;
record Product(String name, String category, int price) {}
public class IntermediateOps {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("노트북", "전자기기", 1500000),
new Product("마우스", "전자기기", 35000),
new Product("키보드", "전자기기", 89000),
new Product("책상", "가구", 250000),
new Product("의자", "가구", 350000),
new Product("모니터", "전자기기", 450000),
new Product("책", "문구", 15000),
new Product("펜", "문구", 3000)
);
// filter: 조건에 맞는 요소만 통과
System.out.println("=== 10만원 이상 ===");
products.stream()
.filter(p -> p.price() >= 100000)
.forEach(p -> System.out.println(p.name() + ": " + p.price()));
// map: 요소를 변환
System.out.println("\n=== 상품명 목록 ===");
products.stream()
.map(Product::name)
.forEach(System.out::println);
// distinct: 중복 제거
System.out.println("\n=== 카테고리 종류 ===");
products.stream()
.map(Product::category)
.distinct()
.forEach(System.out::println);
// sorted: 정렬
System.out.println("\n=== 가격순 정렬 ===");
products.stream()
.sorted((a, b) -> Integer.compare(a.price(), b.price()))
.forEach(p -> System.out.printf("%-6s %,d원%n", p.name(), p.price()));
// peek: 디버깅용 (중간에 값 확인)
System.out.println("\n=== 파이프라인 추적 ===");
long count = products.stream()
.filter(p -> p.price() > 50000)
.peek(p -> System.out.println("필터 통과: " + p.name()))
.map(Product::name)
.count();
System.out.println("결과 수: " + count);
// flatMap: 중첩 구조 평탄화
List<List<String>> nested = List.of(
List.of("a", "b"),
List.of("c", "d"),
List.of("e", "f")
);
nested.stream()
.flatMap(List::stream) // List<List<String>> -> Stream<String>
.forEach(s -> System.out.print(s + " "));
System.out.println();
}
}
최종 연산 (Terminal Operations)
스트림을 소비하여 결과를 만들어내는 연산들입니다.
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
record Employee(String name, String department, int salary) {}
public class TerminalOps {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("홍길동", "개발팀", 5000),
new Employee("김영희", "기획팀", 4500),
new Employee("이철수", "개발팀", 6000),
new Employee("박지민", "디자인팀", 4800),
new Employee("최수진", "개발팀", 5500),
new Employee("정민호", "기획팀", 4200)
);
// count: 개수
long devCount = employees.stream()
.filter(e -> e.department().equals("개발팀"))
.count();
System.out.println("개발팀 인원: " + devCount);
// sum, average, min, max
int totalSalary = employees.stream()
.mapToInt(Employee::salary)
.sum();
System.out.println("총 급여: " + totalSalary);
OptionalInt maxSalary = employees.stream()
.mapToInt(Employee::salary)
.max();
System.out.println("최고 급여: " + maxSalary.orElse(0));
double avgSalary = employees.stream()
.mapToInt(Employee::salary)
.average()
.orElse(0);
System.out.printf("평균 급여: %.0f%n", avgSalary);
// findFirst, findAny
Optional<Employee> firstDev = employees.stream()
.filter(e -> e.department().equals("개발팀"))
.findFirst();
firstDev.ifPresent(e -> System.out.println("첫 개발자: " + e.name()));
// anyMatch, allMatch, noneMatch
boolean hasHighPaid = employees.stream()
.anyMatch(e -> e.salary() > 5500);
System.out.println("5500 초과 있나? " + hasHighPaid);
boolean allPositive = employees.stream()
.allMatch(e -> e.salary() > 0);
System.out.println("모두 양수? " + allPositive);
// reduce: 누적 연산
int sumReduce = employees.stream()
.map(Employee::salary)
.reduce(0, Integer::sum);
System.out.println("reduce 합계: " + sumReduce);
}
}
Collectors를 활용한 수집
스트림 결과를 다양한 컬렉션이나 형태로 수집합니다.
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
record Student(String name, int score, String grade) {}
public class CollectorsExample {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("홍길동", 85, "B"),
new Student("김영희", 92, "A"),
new Student("이철수", 78, "C"),
new Student("박지민", 95, "A"),
new Student("최수진", 88, "B"),
new Student("정민호", 91, "A")
);
// toList, toSet
List<String> nameList = students.stream()
.map(Student::name)
.collect(Collectors.toList());
Set<String> gradeSet = students.stream()
.map(Student::grade)
.collect(Collectors.toSet());
System.out.println("이름: " + nameList);
System.out.println("학점 종류: " + gradeSet);
// joining: 문자열 결합
String allNames = students.stream()
.map(Student::name)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("전체: " + allNames);
// groupingBy: 그룹화
Map<String, List<Student>> byGrade = students.stream()
.collect(Collectors.groupingBy(Student::grade));
byGrade.forEach((grade, list) -> {
System.out.println(grade + "학점: " +
list.stream().map(Student::name).collect(Collectors.joining(", ")));
});
// partitioningBy: 두 그룹으로 분리
Map<Boolean, List<Student>> partition = students.stream()
.collect(Collectors.partitioningBy(s -> s.score() >= 90));
System.out.println("90점 이상: " +
partition.get(true).stream().map(Student::name).toList());
System.out.println("90점 미만: " +
partition.get(false).stream().map(Student::name).toList());
// 통계
var stats = students.stream()
.collect(Collectors.summarizingInt(Student::score));
System.out.println("평균: " + stats.getAverage());
System.out.println("최고: " + stats.getMax());
System.out.println("최저: " + stats.getMin());
System.out.println("합계: " + stats.getSum());
System.out.println("인원: " + stats.getCount());
}
}
오늘의 연습문제
-
매출 분석: 상품 목록(이름, 카테고리, 가격, 수량)에서 스트림을 사용하여 카테고리별 총 매출(가격x수량), 전체 평균 가격, 가장 비싼 상품을 구하세요.
-
문자열 처리 파이프라인: 문장 리스트에서 모든 단어를 추출(flatMap), 소문자 변환, 3글자 이상만 필터링, 중복 제거, 알파벳 정렬하여 최종 리스트로 수집하세요.
-
사원 통계 보고서: 사원 목록에서 부서별 평균 급여, 부서별 최고 급여자 이름, 전체 급여 상위 3명을 스트림으로 구하고 보고서 형태로 출력하세요.